﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using Furiant.Figment.Items.Lootable;

namespace Furiant.Figment.Agents.Inventory
{
    public abstract class InventoryBase : IInventory
    {
        #region Fields

        private readonly IHasInventory _owner;
        private readonly IDictionary<EquipSlotDefinition, IEquippable> _collection;
        private IInventoryUnequipTrap _unequipTrap;
        private readonly IInventoryOverflowTrap _overflowTrap;

        #endregion

        #region Constructors

        /// <summary>
        /// Creates a new instance of InventoryBase and adds it to the specified parent IInventoryCollection.
        /// </summary>
        /// <param name="owner">The IHasInventory object whose Inventory property this instance will represent. </param>
        /// <param name="unequipTrap">The IUnequipTrap for this instance. If null, the default implementation will be used.</param>
        /// <param name="overflowTrap">The IOverflowTrap for this instance. If null, the default implementation will be used.</param>
        protected InventoryBase(
            IHasInventory owner,
            IInventoryUnequipTrap unequipTrap,
            IInventoryOverflowTrap overflowTrap)
        {
            _owner = owner;
            _collection = new Dictionary<EquipSlotDefinition, IEquippable>();
            _unequipTrap = unequipTrap ?? new InventoryUnequipTrap();
            _overflowTrap = overflowTrap ?? new InventoryOverflowTrap();
        }

        #endregion

        #region Properties

        /// <summary>
        /// Gets the IHasInventory object whose Inventory property this instance represents.
        /// </summary>
        public virtual IHasInventory Owner { get { return _owner; } }

        protected virtual IEnumerable<KeyValuePair<EquipSlotDefinition, IEquippable>> Collection { get { return _collection; } }

        /// <summary>
        /// Gets the collection representing the slots into which items may be equipped.
        /// </summary>
        public virtual IEnumerable<EquipSlotDefinition> Keys { get { return _collection.Keys; } }

        /// <summary>
        /// Gets the collection of items currently equipped to their corresponding slots. An element with a null value
        /// indicates that nothing is equipped to that slot.
        /// </summary>
        public virtual IEnumerable<IEquippable> Values { get { return _collection.Values; } }

        public virtual IEquippable this[EquipSlotDefinition key] { get { return _collection[key]; } }

        /// <summary>
        /// Gets the collection into which unequipped items are placed.
        /// </summary>
        public virtual IInventoryUnequipTrap UnequipTrap { get { return _unequipTrap; } protected set { _unequipTrap = value; } }

        /// <summary>
        /// Gets the collection into which loot is placed when it cannot be placed directly into an equipped container.
        /// </summary>
        public virtual IInventoryOverflowTrap OverflowTrap { get { return _overflowTrap; } }

        #endregion

        #region Methods

        /// <summary>
        /// Returns whether the specified key represents an equippable slot in this instance.
        /// </summary>
        /// <param name="key"></param>
        /// <returns></returns>
        public virtual bool ContainsKey(EquipSlotDefinition key)
        {
            return _collection.ContainsKey(key);
        }

        /// <summary>
        /// Returns whether the specified item is currently equipped.
        /// </summary>
        /// <param name="value"></param>
        /// <returns></returns>
        public virtual bool ContainsValue(IEquippable value)
        {
            return _collection.Values.Contains(value);
        }

        /// <summary>
        /// Adds the specified key to the end of the Keys collection.
        /// </summary>
        /// <param name="key">The key to add.</param>
        public virtual void AddSlot(EquipSlotDefinition key)
        {
            _collection.Add(key, null);
        }

        /// <summary>
        /// Adds the specified collection to the end of the Keys collection.
        /// </summary>
        /// <param name="keys">The collection of keys to add.</param>
        public virtual void AddSlotRange(IEnumerable<EquipSlotDefinition> keys)
        {
            foreach (var k in keys.Distinct())
            {
                AddSlot(k);
            }
        }

        /// <summary>
        /// Attemps to equip the specified item to the best found map based on the specified key, provided that all 
        /// EquipActionCriteria are satisfied for equipping the item and unequipping any items in overlapping slots.
        /// Any unequipped items are transferred to the UnequipTrap for this instance. Returns whether the 
        /// EquipActionCriteria were satisfied for the respective action(s). Returns false if the item is already
        /// equipped. Throws an exception if no valid EquipSlotMap could be found, or the Owner of this instance is null.
        /// </summary>
        ///  <param name="item">The item to equip.</param>
        /// <param name="key">The slot on which to base the search for a valid map.</param>
        /// <returns></returns>
        public virtual bool TryEquipItem(IEquippable item, EquipSlotDefinition key)
        {
            if (_owner == null)
            {
                throw new InvalidOperationException(
                    "This instance does not belong to an IHasInventory object.");
            }

            if (ContainsValue(item)) return false;
            
            var targetMap = GetItemBestMap(item, key);

            if (!item.EquipSlotMaps.Contains(targetMap))
            {
                throw new ArgumentException(
                    "The specified EquipSlotMap does not exist in the item's EquipSlotMaps collection.");
            }

            if (!item.EquipActionCriteriaSatisfied(Owner, EquipAction.Equip)) return false;
            
            var overlapItems = GetOverlappingItems(targetMap).ToList();

            if (overlapItems.Any(p => !p.EquipActionCriteriaSatisfied(Owner, EquipAction.Unequip))) return false;

            foreach (var overlapItem in overlapItems)
            {
                ForceUnequipItem(overlapItem);
            }

            _collection[targetMap.Primary] = item;

            return true;
        }

        /// <summary>
        /// Attempts to unequip the specified item from its active EquipSlotMap, provided that its EquipActionCriteria
        /// are satisfied for the action. Returns false if the EquipActionCriteria were not satisfied. Returns true if
        /// the operation was successful. The unequipped item is transferred to the UnequipTrap for this instance. 
        /// Throws an exception if the specified item is not already equipped.
        /// </summary>
        /// <param name="item">The item to unequip.</param>
        /// <returns></returns>
        public virtual bool TryUnequipItem(IEquippable item)
        {
            if (!item.EquipActionCriteriaSatisfied(Owner, EquipAction.Unequip))
            {
                return false;
            }

            ForceUnequipItem(item);

            return true;
        }

        /// <summary>
        /// Removes an equipped item from the Values collection and transfers it to the UnequipTrap collection. Throws an 
        /// exception if the specified item is not already equipped.
        /// </summary>
        /// <param name="item">The item to unequip.</param>
        /// <returns></returns>
        public virtual void ForceUnequipItem(IEquippable item)
        {
            if (!ContainsValue(item))
            {
                throw new ArgumentException("The specified item is not equipped.");
            }

            var map = GetItemActiveMap(item);
            _collection[map.Primary] = null;
            _unequipTrap.Add(item);
        }

        /// <summary>
        /// Returns an EquipSlotMap from the specified item's EquipSlotMaps collection. The method first attempts to find a 
        /// map whose Primary slot is the specified key. If no such map is found, it will next attempt to find the first map 
        /// whose Referred slots contain the specified slot. Throws an exception if no valid maps are found.
        /// </summary>
        /// <param name="item">The item for which to find a map.</param>
        /// <param name="key">The slot on which to base the search.</param>
        /// <returns></returns>
        public virtual EquipSlotMap GetItemBestMap(IEquippable item, EquipSlotDefinition key)
        {
            var map = item.EquipSlotMaps.Single(p => p.Primary == key);
            var targetMap = map ?? item.EquipSlotMaps.First(p => p.Referred.Contains(key));

            if (targetMap == null)
            {
                throw new ArgumentException("The item has no EquipSlotMap which contains the specified key.", "key");
            }

            return targetMap;
        }

        /// <summary>
        /// Gets the EquipSlotMap to which the specified item is currently equipped. Returns null if the item is not equipped.
        /// </summary>
        /// <param name="item">The equipped item whose active map should be returned.</param>
        /// <returns></returns>
        public virtual EquipSlotMap GetItemActiveMap(IEquippable item)
        {
            var key = _collection.Single(p => p.Value == item).Key;

            return key != null ? item.EquipSlotMaps[key] : null;
        }

        /// <summary>
        /// Gets a collection of EquipSlotMap which represents the active maps of any items which are currently equipped
        /// in slots which overlap the specified map.
        /// </summary>
        /// <param name="map">The map whose slots to find in active maps of equipped items.</param>
        /// <returns></returns>
        public virtual IEnumerable<EquipSlotMap> GetOverlappingMaps(EquipSlotMap map)
        {
            var overlap = new List<EquipSlotMap>();

            foreach (var slot in map)
            {
                var item = _collection[slot];

                if (item != null)
                {
                    overlap.Add(GetItemActiveMap(item));
                }
            }

            return overlap;
        }

        /// <summary>
        /// Gets a collection of EquipSlotDefinition which represents the distinct slots occupied by any items which are
        /// currently equipped in slots which overlap the specified map.
        /// </summary>
        /// <param name="map"></param>
        /// <returns></returns>
        public virtual IEnumerable<EquipSlotDefinition> GetOverlappingMapsSlots(EquipSlotMap map)
        {
            var slots = new List<EquipSlotDefinition>();

            var overlap = GetOverlappingMaps(map);

            foreach (var foundMap in overlap)
            {
                slots.AddRange(foundMap.Where(map.Contains));
            }

            return slots.Distinct();
        }

        /// <summary>
        /// Gets a collection of items which are equipped to maps which overlap the slots in the specified map.
        /// </summary>
        /// <param name="map"></param>
        /// <returns></returns>
        public virtual IEnumerable<IEquippable> GetOverlappingItems(EquipSlotMap map)
        {
            return map.Select(slot => _collection[slot]).Where(item => item != null).ToList();
        }

        public virtual IEnumerator<KeyValuePair<EquipSlotDefinition, IEquippable>> GetEnumerator()
        {
            return _collection.GetEnumerator();
        }

        IEnumerator IEnumerable.GetEnumerator()
        {
            return GetEnumerator();
        }

        #endregion
    }
}
